home *** CD-ROM | disk | FTP | other *** search
/ Developer CD Series 2000 November: Tool Chest / Dev.CD Nov 00 TC Disk 2.toast / pc / tool chest / development kits / mpw related / lurkers 1.7 / lurkers.c < prev    next >
Encoding:
C/C++ Source or Header  |  1997-11-11  |  30.9 KB  |  1,072 lines

  1. /*----------------------------------------------------------------------------------------
  2.  
  3.      Lurkers
  4.      
  5.      Greg Anderson
  6.      January 1994
  7.      
  8.      AppleLink: G.ANDERSON
  9.      Internet:  greggor@apple.com
  10.      
  11.      Modified by Greg Branche, gregb@apple.com
  12.      October 1997
  13.      
  14.      About Lurkers:
  15.      
  16.      Lurkers searches a directory for files that are modifiable, including files
  17.      not in a project, files checked out for modification, and modify-read-only
  18.      files.
  19.      
  20.      Search modes (pick any one):
  21.      
  22.         -modifiable            (default) Search for files not in a project, files
  23.                             checked out for modification, and modify-read-only files
  24.         -notmodifiable        Search for files checked into a project that are not
  25.                             modifiable (has 'ckid', but is neither MRO nor checked out
  26.                             for modification)
  27.         -mro                Search for files that are checked into a project (has 'ckid'),
  28.                             but have been modified-read-only.
  29.         -insomeproject        Search for files with a 'ckid' resource--modifiable or not
  30.         -notinanyproject    Search for files without a 'ckid' resource
  31.         -modifiableinproject    Search for files with a 'ckid' resource that are MRO'ed or
  32.                             checked out for modification (does not actually check
  33.                             project database to see if any changes have been made, though)
  34.         -checkedoutmodifiable    Searches for files with a 'ckid' resource that are
  35.                             checked out for modification.
  36.     
  37.     Other flags:
  38.     
  39.         -r                    Search specified directory recursively
  40.         -s                    Short output (only list names of files that match
  41.                             the search criterion)
  42.         -f                    Output full pathnames
  43.         -q                    Don't quote filenames with special characters
  44.                             (Quoting only done in short output)
  45.         -p                    Show progress information
  46.         -rev                Show the revision number of the file
  47.         -textonly            Ignore files whose type is not TEXT
  48.         -ignore n            Ignore files and folders whose name is 'n'
  49.         
  50.     Revision History:
  51.     
  52.         1.0, 19 Jan 94        Initial release
  53.         1.1, 20 Jan 94        Lurkers being too strict in its test for checked out
  54.                             files, so it missed some.  Also, I failed to check
  55.                             for errors when opening the resource fork of a file
  56.                             (oops!), so files without a resource fork reported
  57.                             the information applicable to the next item in the
  58.                             resource chain (not a problem unless you check your
  59.                             MPW shell into a project--but our team does!)
  60.         1.2, 25 Jan 94        Lurkers still missing files; make its test even less
  61.                             strict (pretty sure I got it right this time)
  62.         1.3, 27 Jan 94        Added flags -f -q -rev -textonly
  63.         1.4, 16    Feb    94        Added partial pathnames, -checkedoutmodifiable, -mro
  64.         1.5, 2 Jun 94        Andy wanted me to remove some spurrious blank lines in -s mode
  65.         1.6, 9 Jun 94        Improved help, added -ignore, allowed single files to
  66.                             be tested
  67.         1.7, 7 Oct 97        Added support for all special shell characters that must be quoted.
  68.                             Did one of the "To Do"s that was left over from previous versions. (i.e.,
  69.                             quoting of single quotes within a name is handled properly)
  70.         
  71.     To Do:
  72.     
  73.         It would also be nice if -ignore handled wildcards, or even regular expresions
  74.  
  75. ----------------------------------------------------------------------------------------*/
  76.  
  77. #include    <Types.h>
  78. #include     <ctype.h>
  79. #include     <fcntl.h>
  80. #include     <string.h>
  81. #include     <stdio.h>
  82. #include    <StdLib.h>
  83. #include    <ErrMgr.h>
  84. #include    <CursorCtl.h>
  85. #include    <Errors.h>
  86. #include    <QuickDraw.h>
  87. #include    <Files.h>
  88. #include    <Memory.h>
  89. #include    <Resources.h>
  90. #include    <CursorCtl.h>
  91. #include    <PLStringFuncs.h>
  92.  
  93. //
  94. // First line of usage string (more than this is printed out)
  95. //
  96. static    char*    usage = "%s [file's projector status] [option…] directory|file…\n";
  97.  
  98. static    long    optionsSpecified = false;
  99.  
  100. //
  101. // We know a little bit about the contents of a ckid resource:
  102. //
  103. struct ckidheader
  104. {
  105.     long        fUnknownID;                // Different after every projector operation
  106.     long        fUnkownConstant;        // Always the same
  107.     
  108.     short        fUnknown1;                // Usually 0004
  109.     short        fIsCheckedOut;            // Usually 2, 3 or 4 if checked out, always 0 if not
  110.     
  111.     short        fIsMRO;                    // 0001 if MRO, 0000 if not 
  112.     short        fUnknown2;                // Usually 0000
  113.     
  114.     long        fUnknown3;                // I don't know or care what these are
  115.     long        fUnknown4;
  116.     long        fUnknown5;
  117.     long        fUnknown6;
  118.     long        fUnknown7;
  119.     long        fUnknown8;
  120.     
  121.     unsigned char fProjectName;            // null-terminated pascal string
  122.     
  123.                                         // After the project name is the name of the
  124.                                         // person who checked out this file on this
  125.                                         // machine (no relation to the person who currently
  126.                                         // has the file checked out).  Also null-terminated
  127.                                         
  128.                                         // After the user name is the revision number
  129.                                         // of the file, in ascii and null-terminated.
  130.     
  131. };
  132.  
  133. //
  134. // Projector states that we know about:
  135. // (One and only one of these bits may be set at any time, as we switch on them)
  136. //
  137. #define kNotModifiable            1
  138. #define kModifiedReadOnly        2
  139. #define kCheckedOut                4
  140. #define kNotInAProject            8
  141.  
  142. //
  143. // This mask must include all of the file project states above
  144. //
  145. #define kFileProjectMask        (kNotModifiable | kModifiedReadOnly | kCheckedOut | kNotInAProject)
  146.  
  147. //
  148. // Here are some extra file project states that we do not switch on
  149. // (so these bits may be set in addition to the ones above)
  150. //
  151. #define kHasCKIDAndIsModifiable    0x10
  152. #define kHasCKIDAndIsCheckedOut    0x20
  153.  
  154. //
  155. // Global options, because there is no need to pass it around
  156. //
  157. long gOptions = 0;
  158. long gWantOutputForState = kModifiedReadOnly | kCheckedOut | kNotInAProject;
  159.  
  160. #define kShortOutput        1
  161. #define kRecursive            2
  162. #define kOutputRevision        4
  163. #define kTextOnly            8
  164. #define kFullPathnames        0x10
  165. #define kDontQuote            0x20
  166. #define kShowProgress        0x80000000
  167.  
  168. #define kMagicUnusedDirID    0x8000000
  169.  
  170. typedef enum {
  171.     noQuoting, standardQuoting, specialQuoting
  172. } QuoteType;
  173.  
  174. //
  175. // Some global variables that should be local, but I don't
  176. // want my stack to get too large, and I don't feel like
  177. // being clever.
  178. //
  179. // The code is very carefully written to avoid using these
  180. // variables after they have been reused in a recursive call.
  181. // This is EVIL, but we want to avoid having a large stack.
  182. //
  183. CInfoPBRec pb;
  184. Str255 gFilename;
  185. Str255 gFolderpath;
  186.  
  187. Str255 gTempProjectName;
  188. Str31 gTempRevisionNumber;
  189. char gTempRevisionString[64];
  190.  
  191. char** gIgnoreList;
  192. short gNumberOfIgnoreItems = 0;
  193.  
  194. #define ConvertToUppercase(c)    (((c >= 'a') && (c <= 'z')) ? c - 'a' + 'A' : c)
  195.  
  196. //----------------------------------------------------------------------------------------
  197. // OptionSpecified: 
  198. //----------------------------------------------------------------------------------------
  199. Boolean OptionSpecified(long flag)
  200. {
  201.     return ((gOptions & flag) != 0);
  202. } // OptionSpecified 
  203.  
  204. //----------------------------------------------------------------------------------------
  205. // ItemMatches
  206. //
  207. // It would be nice if this checked for wildcards...
  208. //----------------------------------------------------------------------------------------
  209. Boolean ItemMatches(Str255 testItem, char* compareWith)
  210. {
  211.     Boolean itemMatches = true;
  212.     short i;
  213.     
  214.     for(i=1; i<=testItem[0]; ++i)
  215.     {
  216.         unsigned char c1 = testItem[i];
  217.         unsigned char c2 = *compareWith++;
  218.         if(ConvertToUppercase(c1) != ConvertToUppercase(c2))
  219.         {
  220.             itemMatches = false;
  221.             break;
  222.         }
  223.     }
  224.     
  225.     //
  226.     // If we ran out of characters in 'testItem', but there
  227.     // are still characters left in 'compareWith', then the
  228.     // two items don't really match
  229.     //
  230.     if(*compareWith != 0)
  231.         itemMatches = false;
  232.     
  233.     return itemMatches;
  234. } // ItemMatches
  235.  
  236. //----------------------------------------------------------------------------------------
  237. // ItemIsInIgnoreList
  238. //
  239. // Return 'true' if the specified string is in the ignore list
  240. //----------------------------------------------------------------------------------------
  241. Boolean ItemIsInIgnoreList(Str255 testItem)
  242. {
  243.     Boolean shouldIgnore = false;
  244.     short i;
  245.     
  246.     for(i=0; i< gNumberOfIgnoreItems; ++i)
  247.     {
  248.         if(ItemMatches(testItem, gIgnoreList[i]))
  249.         {
  250.             shouldIgnore = true;
  251.             break;
  252.         }
  253.     }
  254.     
  255.     return shouldIgnore;
  256. } // ItemIsInIgnoreList
  257.  
  258. //----------------------------------------------------------------------------------------
  259. // BuildFolderPathname: 
  260. //----------------------------------------------------------------------------------------
  261. void BuildFolderPathname(short vRefNum, long dirID, Str255 folderpath, long stopDirID, Boolean firstTime)
  262. {
  263.     Str63 thisName;
  264.     OSErr err = noErr;
  265.     
  266.     thisName[0] = 0;
  267.     folderpath[0] = 0;
  268.         
  269.     pb.dirInfo.ioCompletion            = nil;
  270.     pb.dirInfo.ioNamePtr            = thisName;
  271.     pb.dirInfo.ioResult                = noErr;
  272.     pb.dirInfo.ioVRefNum            = vRefNum;
  273.     pb.dirInfo.ioDrDirID            = dirID;
  274.     pb.dirInfo.ioFDirIndex            = -1;
  275.     
  276.     err = PBGetCatInfo(&pb,false);
  277.     
  278.     if(err == noErr)
  279.     {
  280.         if(dirID != stopDirID)
  281.         {
  282.             BuildFolderPathname(vRefNum, pb.dirInfo.ioDrParID, folderpath, stopDirID, false);
  283.  
  284.             //
  285.             // Append 'thisName' onto folderpath
  286.             //
  287.             thisName[thisName[0] + 1] = 0;
  288.             strcpy((char *) folderpath + folderpath[0] + 1, (char *) thisName + 1);
  289.             folderpath[0] += thisName[0];
  290.             
  291.             //
  292.             // Append a colon onto folderpath
  293.             //
  294.             folderpath[folderpath[0] + 1] = ':';
  295.             ++folderpath[0];
  296.         }
  297.         else if(!firstTime)
  298.         {
  299.             folderpath[1] = ':';
  300.             folderpath[0] = 1;
  301.         }
  302.     }
  303. } // BuildFolderPathname 
  304.  
  305. //----------------------------------------------------------------------------------------
  306. // ClassifyCharacter: 
  307. //----------------------------------------------------------------------------------------
  308. QuoteType ClassifyCharacter(char c)
  309. {
  310.     switch(c)
  311.     {
  312.     case '\t':        // Tab also separates words.
  313.     case '\n':        // Return separates commands.
  314.     case '\'':        // Single quote quotes all other characters.
  315.         return(specialQuoting);
  316.         break;
  317.  
  318.     case ' ':        // Space separates words.
  319.     case ';':        // Semicolon also separates commands.
  320.     case '|':        // Pipe separates commands and pipes output to input.
  321.     case '&':        // And separates commands, executing second if first succeeds.
  322.     case '(':        // 
  323.     case ')':        // Parenthesis group commands.
  324.     case '#':        // Comment begins comments.
  325.     case '\"':        // Double quote quotes all characters except ∂, {, and `.
  326.     case '∂':        // Escape (Option-D) quotes the following character.
  327.     case '/':        // Slash quotes all characters except ∂, {, and `.
  328.     case '\\':        // Backslash quotes all characters except ∂, {, and `.
  329.     case '{':
  330.     case '}':        // Braces denote variable substitution.
  331.     case '`':        // Backquotes denote command substitution.
  332.     case '\?':        // Question mark matches any character in filename patterns.
  333.     case '≈':        // Approximately (Option-X) matches any string in patterns.
  334.     case '[':
  335.     case ']':        // Brackets enclose character sets in filename patterns.
  336.     case '*':        // Star indicates zero or more repetitions in patterns.
  337.     case '+':        // Plus indicates one or more repetitions in patterns.
  338.     case '«':
  339.     case '»':        // European quotes (Option-\ and Option-Shift-\) enclose repeat counts.
  340.     case '<':        // Less-than indicates an input file specification.
  341.     case '>':        // Greater-than indicates an output file specification.
  342.     case '≥':        // Greater-than-or-equal indicates a diagnostic specification.
  343.     case '∑':        // Capital sigma (option-w) indicates both an output file and diagnostic output file specification.
  344.     case '…':        // Ellipsis (Option-;) signals the Shell to use Commando
  345.         return(standardQuoting);
  346.         break;
  347.     
  348.     default:
  349.         return(noQuoting);
  350.     }
  351.     
  352.     return(noQuoting);    // we gotta return _something_!
  353. } // ClassifyCharacter
  354.  
  355. //----------------------------------------------------------------------------------------
  356. // HasSpecialCharacters: 
  357. //----------------------------------------------------------------------------------------
  358. QuoteType HasSpecialCharacters(Str255 pstring)
  359. {
  360.     QuoteType hasSpecial = noQuoting;
  361.     short i;
  362.     
  363.     for(i = 1; i <= pstring[0]; ++i)
  364.     {
  365.         QuoteType isSpecial = ClassifyCharacter(pstring[i]);
  366.  
  367.         if(isSpecial > hasSpecial)
  368.             if((hasSpecial = isSpecial) == specialQuoting)
  369.                 break;    // no reason to continue searching, if we've already found the most restrictive character type
  370.     }
  371.  
  372.     return hasSpecial;
  373. } // HasSpecialCharacters 
  374.  
  375. //----------------------------------------------------------------------------------------
  376. // QuoteString: 
  377. //----------------------------------------------------------------------------------------
  378. void QuoteString(Str255 theString, Boolean *withinQuotes)
  379. {
  380.     int        i;
  381.     char    toPrint;
  382.     
  383.     for(i = 1; i <= theString[0]; ++i)
  384.     {
  385.         if(ClassifyCharacter(theString[i]) == specialQuoting)
  386.         {
  387.             if(*withinQuotes)
  388.             {
  389.                 putchar('\'');
  390.                 *withinQuotes = false;
  391.             }
  392.             switch(theString[i])
  393.             {
  394.             case '\t': toPrint = 't'; break;        // Tab also separates words.
  395.             case '\n': toPrint = 'n'; break;        // Return separates commands.
  396.             case '\'': toPrint = '\''; break;        // Single quote quotes all other characters.
  397.             default: toPrint = theString[i];
  398.             }
  399.             fprintf(stdout, "∂%c", toPrint);
  400.         }
  401.         else
  402.         {
  403.             if(!*withinQuotes)
  404.             {
  405.                 putchar('\'');
  406.                 *withinQuotes = true;
  407.             }
  408.             putchar(theString[i]);
  409.         }
  410.     }
  411. } // QuoteString
  412.  
  413. //----------------------------------------------------------------------------------------
  414. // ExamineProjectorInformation: 
  415. //----------------------------------------------------------------------------------------
  416. long ExamineProjectorInformation(short vRefNum, long dirID, const Str255 fileName, Str255 projectName, Str31 revisionNumber)
  417. {
  418.     Handle ckidHandle = nil;
  419.     short resRefNum;
  420.     long projectorInfo = kNotInAProject;
  421.     
  422.     projectName[0] = 0;
  423.     revisionNumber[0] = 0;
  424.     
  425.     resRefNum = HOpenResFile(vRefNum, dirID, fileName, fsRdPerm);
  426.     
  427.     if(resRefNum != -1)
  428.     {
  429.         ckidHandle = Get1Resource('ckid', 128);
  430.         if(ckidHandle != nil)
  431.         {
  432.             struct ckidheader* ckpeek = nil;
  433.             unsigned char* p;
  434.             
  435.             HLock(ckidHandle);
  436.             ckpeek = *( (struct ckidheader**)ckidHandle);
  437.             
  438.             //
  439.             // Look at a bit of the ckid resource
  440.             //
  441.             if(ckpeek->fIsCheckedOut != 0)
  442.                 projectorInfo = kCheckedOut;
  443.             else if((ckpeek->fIsMRO & 1) != 0)
  444.                 projectorInfo = kModifiedReadOnly;
  445.             else
  446.                 projectorInfo = kNotModifiable;
  447.             
  448.             //
  449.             // Set the 'changed' bit if this item
  450.             // is modifiable (we know that it is in a project)
  451.             //
  452.             if((projectorInfo & (kModifiedReadOnly | kCheckedOut)) != 0)
  453.                 projectorInfo |= kHasCKIDAndIsModifiable;
  454.             if((projectorInfo & kCheckedOut) != 0)
  455.                 projectorInfo |= kHasCKIDAndIsCheckedOut;
  456.             
  457.             //
  458.             // Hack:  'projectName' and fProjectName are
  459.             // pascal strings, but they are always
  460.             // null-terminated, so we can strcpy it
  461.             //
  462.             strcpy((char *) projectName, (char *) &ckpeek->fProjectName);
  463.                         
  464.             //
  465.             // The string past the project name is the user name
  466.             // (add one to skip the null termination)
  467.             //
  468.             p = &ckpeek->fProjectName + strlen((char *) &ckpeek->fProjectName) + 1;
  469.             
  470.             //
  471.             // The string past the user name is the revision number
  472.             // (add one to skip the null termination)
  473.             // Once again, we use strcpy, as the string is always
  474.             // null-terminated.
  475.             //
  476.             p = p + strlen((char *) p) + 1;
  477.             strcpy((char *) revisionNumber, (char *) p);
  478.             
  479.             HUnlock(ckidHandle);
  480.         }
  481.         
  482.         CloseResFile(resRefNum);
  483.     }
  484.     //else
  485.     //    fprintf(stderr, "### Error opening resource fork of %P", fileName);
  486.     
  487.     return projectorInfo;
  488. } // ExamineProjectorInformation 
  489.  
  490. //----------------------------------------------------------------------------------------
  491. // ProcessFile: 
  492. //----------------------------------------------------------------------------------------
  493. void ProcessFile(short vRefNum, long dirID, Str255 folderpath, Str255 filename, Boolean* didOutput)
  494. {
  495.     char commaString[4];
  496.     long fileProjectStatus = 0;
  497.     
  498.     //
  499.     // Don't do a thing with this item if it's in the ignore list
  500.     //
  501.     if(ItemIsInIgnoreList(filename) == false)
  502.     {
  503.         //
  504.         // Find out if the file has a 'ckid' resource.  While we're at it,
  505.         // extract information about the file (checked out, MRO'ed, etc, project
  506.         // name, revision number)
  507.         //
  508.         fileProjectStatus = ExamineProjectorInformation(vRefNum, dirID, filename, gTempProjectName, gTempRevisionNumber);
  509.         
  510.         //
  511.         // Ignore the file unless we want output for its state
  512.         //
  513.         if((gWantOutputForState & fileProjectStatus) != 0)
  514.         {
  515.             //
  516.             // If doing short output (-s), only print the name of the file
  517.             //
  518.             if(OptionSpecified(kShortOutput))
  519.             {
  520.                 //
  521.                 // Print revision numbers if requested (-rev)
  522.                 //
  523.                 if((OptionSpecified(kOutputRevision)) && (gTempRevisionNumber[0] != 0))
  524.                 {
  525.                     strcpy(commaString, ",");
  526.                 }
  527.                 else
  528.                 {
  529.                     commaString[0] = 0;
  530.                     gTempRevisionNumber[0] = 0;
  531.                 }
  532.                 
  533.                 //
  534.                 // Quote the string if necessary
  535.                 //
  536.                 if(OptionSpecified(kDontQuote) == true)
  537.                 {
  538.                     fprintf(stdout, "%P%P%s%P\n", folderpath, filename, commaString, gTempRevisionNumber);
  539.                 }
  540.                 else
  541.                 {
  542.                     QuoteType    pathQuotingNeeded, fileQuotingNeeded;
  543.                     
  544.                     pathQuotingNeeded = HasSpecialCharacters(folderpath);
  545.                     fileQuotingNeeded = HasSpecialCharacters(filename);
  546.                     
  547.                     
  548.                     if(pathQuotingNeeded == noQuoting && fileQuotingNeeded == noQuoting)
  549.                     {    // no special characters found, just print the darn thang!
  550.                         fprintf(stdout, "%P%P%s%P\n", folderpath, filename, commaString, gTempRevisionNumber);
  551.                     }
  552.                     else if(pathQuotingNeeded < specialQuoting && fileQuotingNeeded < specialQuoting)
  553.                     {    // we can just quote the whole pathname...
  554.                         fprintf(stdout, "'%P%P%s%P'\n", folderpath, filename, commaString, gTempRevisionNumber);
  555.                     }
  556.                     else
  557.                     {    // Here, we have to parse the string to be printed, so that the proper single-character quoting can be emitted
  558.                         Boolean withinQuotes = false;
  559.  
  560.                         // First, the folder portion of the pathname...
  561.                         QuoteString(folderpath, &withinQuotes);
  562.  
  563.                         // then the filename
  564.                         QuoteString(filename, &withinQuotes);
  565.  
  566.                         // finish up...
  567.                         if(gTempRevisionNumber[0] != 0)
  568.                         {
  569.                             if(!withinQuotes)
  570.                             {
  571.                                 putchar('\'');
  572.                                 withinQuotes = true;
  573.                             }
  574.                             fprintf(stdout, "%s%P", commaString, gTempRevisionNumber);
  575.                         }
  576.                         if(withinQuotes)
  577.                         {
  578.                             putchar('\'');
  579.                             withinQuotes = false;
  580.                         }
  581.                         putchar('\n');
  582.                     }
  583.                 }
  584.                 
  585.             }
  586.             else
  587.             {
  588.                 if(OptionSpecified(kOutputRevision))
  589.                     sprintf(gTempRevisionString, "(%P,%P) ", filename, gTempRevisionNumber);
  590.                 else
  591.                     gTempRevisionString[0] = 0;
  592.                 
  593.                 if(PLstrchr(folderpath, '\n') != NULL || PLstrchr(filename, '\n') != NULL)
  594.                 {
  595.                     Boolean inQuotes = true;
  596.                     fputs("File \'", stdout);
  597.                     QuoteString(folderpath, &inQuotes);
  598.                     QuoteString(filename, &inQuotes);
  599.                     if(inQuotes)
  600.                         fputc('\'', stdout);
  601.                     fputs("; # ", stdout);
  602.                 }
  603.                 else
  604.                 {
  605.                     fprintf(stdout, "File \"%P%P\"; # ", folderpath, filename);
  606.                 }
  607.                 
  608.                 switch(fileProjectStatus & kFileProjectMask)
  609.                 {
  610.                 case kNotInAProject:
  611.                     fprintf(stdout, "is not in a project\n");
  612.                     break;
  613.                 
  614.                 case kNotModifiable:
  615.                     fprintf(stdout, "%sis an unmodifiable copy of a file in the project %P\n", gTempRevisionString, gTempProjectName);
  616.                     break;
  617.                     
  618.                 case kModifiedReadOnly:
  619.                     fprintf(stdout, "%sis a modify-read-only copy of a file in the project %P\n", gTempRevisionString, gTempProjectName);
  620.                     break;
  621.                 
  622.                 case kCheckedOut:
  623.                     fprintf(stdout, "%sis a modifiable copy of a file in the project %P\n", gTempRevisionString, gTempProjectName);
  624.                     break;
  625.                 
  626.                 default:
  627.                     fprintf(stdout, "%sis not recognized by Lurkers\n", gTempRevisionString);
  628.                     break;
  629.                 }
  630.                 
  631.                 //
  632.                 // didOutput actually means "printed a long line"; short lines don't
  633.                 // count.  (This flag is used to determine if a trailing blank line
  634.                 // should be added to the output)
  635.                 //
  636.                 *didOutput = true;
  637.             }
  638.         }
  639.     }
  640. } // ProcessFile 
  641.  
  642. //----------------------------------------------------------------------------------------
  643. // Lurkers: 
  644. //----------------------------------------------------------------------------------------
  645. void Lurkers(short vRefNum, long dirID, long startLurkingDirID)
  646. {
  647.     OSErr err = noErr;
  648.     short index = 1;
  649.     Boolean didOutput = false;
  650.     
  651.     //
  652.     // Build the pathname to this folder if full paths were
  653.     // specified or if we're not doing short output
  654.     //
  655.     gFolderpath[0] = 0;
  656.     if(OptionSpecified(kFullPathnames) || OptionSpecified(kShortOutput))
  657.         BuildFolderPathname(vRefNum, dirID, gFolderpath, OptionSpecified(kShortOutput) && !OptionSpecified(kFullPathnames) ? startLurkingDirID : kMagicUnusedDirID, true);
  658.  
  659.     //
  660.     // If doing long output, tell the user what we're doing
  661.     //
  662.     if(OptionSpecified(kShowProgress))
  663.     {
  664.         fflush(stdout);
  665.         fprintf(stderr, "### Scanning folder \"%P\"\n", gFolderpath);
  666.     }
  667.     
  668.     //
  669.     // Get rid of the pathname if we're not going to output it.
  670.     //
  671.     //if(OptionSpecified(kFullPathnames) == false)
  672.     //    gFolderpath[0] = 0;
  673.     
  674.     //
  675.     // Walk every file in this directory
  676.     //
  677.     while(err == noErr)
  678.     {
  679.         SpinCursor(1);
  680.         
  681.         gFilename[0] = 0;
  682.         
  683.         pb.hFileInfo.ioCompletion        = nil;
  684.         pb.hFileInfo.ioNamePtr            = gFilename;
  685.         pb.hFileInfo.ioResult            = noErr;
  686.         pb.hFileInfo.ioVRefNum            = vRefNum;
  687.         pb.hFileInfo.ioDirID            = dirID;
  688.         pb.hFileInfo.ioFDirIndex        = index;
  689.         
  690.         err = PBGetCatInfo(&pb,false);
  691.         
  692.         //
  693.         // We only care about files right now...
  694.         //
  695.         if((err == noErr) && ((pb.hFileInfo.ioFlAttrib & (1 << 4)) == 0))
  696.         {
  697.             //
  698.             // If 'kTextOnly' is set, only process the file if its type
  699.             // is 'TEXT'.  Otherwise, always process the file
  700.             //
  701.             if( (OptionSpecified(kTextOnly) == false) || (pb.hFileInfo.ioFlFndrInfo.fdType == 'TEXT'))
  702.                 ProcessFile(vRefNum, dirID, gFolderpath, gFilename, &didOutput);
  703.         }
  704.         
  705.         ++index;
  706.     }
  707.     
  708.     //
  709.     // Add another blank line if there was any output,
  710.     // then flush stdout so that the text is actually
  711.     // printed
  712.     //
  713.     if(didOutput)
  714.     {
  715.         putchar('\n');
  716.     }
  717.     fflush(stdout);
  718.     
  719.     //
  720.     // Do we want to do a deep search?
  721.     //
  722.     if(OptionSpecified(kRecursive))
  723.     {
  724.         index = 1;
  725.         err = noErr;
  726.         
  727.         //
  728.         // Walk every folder in this directory
  729.         //
  730.         while(err == noErr)
  731.         {
  732.             gFilename[0] = 0;
  733.             
  734.             pb.dirInfo.ioCompletion            = nil;
  735.             pb.dirInfo.ioNamePtr            = gFilename;
  736.             pb.dirInfo.ioResult                = noErr;
  737.             pb.dirInfo.ioVRefNum            = vRefNum;
  738.             pb.dirInfo.ioDrDirID            = dirID;
  739.             pb.dirInfo.ioFDirIndex            = index;
  740.             
  741.             err = PBGetCatInfo(&pb,false);
  742.             
  743.             //
  744.             // Call Lurkers again on every folder we find...
  745.             //
  746.             if((err == noErr) && ((pb.hFileInfo.ioFlAttrib & (1 << 4)) != 0))
  747.             {
  748.                 //
  749.                 // Don't do the recursive call if this folder's name
  750.                 // appears in the ignore list
  751.                 //
  752.                 if(ItemIsInIgnoreList(gFilename) == false)
  753.                 {
  754.                     Lurkers(vRefNum, pb.dirInfo.ioDrDirID, startLurkingDirID);
  755.                 }
  756.             }
  757.             
  758.             ++index;
  759.         }
  760.     }
  761. } // Lurkers 
  762.  
  763.  
  764. //----------------------------------------------------------------------------------------
  765. // main: 
  766. //----------------------------------------------------------------------------------------
  767. main( int argc, char* argv[] )
  768. {
  769.     Boolean                    optionsSpecified = false;
  770.     Boolean                    didLurkers = false;
  771.     short                    vRefNum = 0;
  772.     long                    dirID = 0;
  773.     short                    parms;
  774.     short                    status = 0;
  775.     Boolean*                processedParameters;
  776.     OSErr                    err = noErr;
  777.     
  778.     InitCursorCtl(nil);
  779.  
  780.     //
  781.     // Set up the ignore list and the set of flags that indicates which
  782.     // parameters we've processed.  Both arrays only need to be 'argc'
  783.     // elements in size, or smaller.
  784.     //
  785.     processedParameters = (Boolean*)NewPtr(sizeof(Boolean) * (argc + 1));
  786.     gIgnoreList = (char**)NewPtr(sizeof(Ptr) * (argc + 1));
  787.     for( parms = 0; parms < argc; ++parms )
  788.     {
  789.         processedParameters[parms] = false;
  790.         gIgnoreList[parms] = nil;
  791.     }
  792.     
  793.     //
  794.     // Run through the parameters once looking for things that start with "-"
  795.     //
  796.     for( parms = 1; parms < argc; parms++ )
  797.     {
  798.         short length = strlen(argv[parms]);
  799.         
  800.         //
  801.         // Look at all of the parameters that have a dash
  802.         //
  803.         if( argv[parms][0] == '-')
  804.         {
  805.             processedParameters[parms] = true;
  806.             
  807.             //
  808.             // Process the specific flags
  809.             //
  810.             if( strcmp( argv[parms], "-modifiable" ) == 0 )
  811.             {
  812.                 optionsSpecified = true;
  813.                 gWantOutputForState = kModifiedReadOnly | kCheckedOut | kNotInAProject;
  814.             }
  815.             else if( strcmp( argv[parms], "-notmodifiable" ) == 0 )
  816.             {
  817.                 optionsSpecified = true;
  818.                 gWantOutputForState = kNotModifiable;
  819.             }
  820.             else if( strcmp( argv[parms], "-mro" ) == 0 )
  821.             {
  822.                 optionsSpecified = true;
  823.                 gWantOutputForState = kModifiedReadOnly;
  824.             }
  825.             else if( strcmp( argv[parms], "-notinanyproject" ) == 0 )
  826.             {
  827.                 optionsSpecified = true;
  828.                 gWantOutputForState = kNotInAProject;
  829.             }
  830.             else if( strcmp( argv[parms], "-insomeproject" ) == 0 )
  831.             {
  832.                 optionsSpecified = true;
  833.                 gWantOutputForState = kModifiedReadOnly | kCheckedOut | kNotModifiable;
  834.             }
  835.             else if( strcmp( argv[parms], "-modifiableinproject" ) == 0 )
  836.             {
  837.                 optionsSpecified = true;
  838.                 gWantOutputForState = kHasCKIDAndIsModifiable;
  839.             }
  840.             else if( strcmp( argv[parms], "-checkedoutmodifiable" ) == 0 )
  841.             {
  842.                 optionsSpecified = true;
  843.                 gWantOutputForState = kHasCKIDAndIsCheckedOut;
  844.             }
  845.             else if( strcmp( argv[parms], "-r" ) == 0 )
  846.             {
  847.                 optionsSpecified = true;
  848.                 gOptions |= kRecursive;
  849.             }
  850.             else if( strcmp( argv[parms], "-s" ) == 0 )
  851.             {
  852.                 optionsSpecified = true;
  853.                 gOptions |= kShortOutput;
  854.             }
  855.             else if( strcmp( argv[parms], "-f" ) == 0 )
  856.             {
  857.                 optionsSpecified = true;
  858.                 gOptions |= kFullPathnames;
  859.             }
  860.             else if( strcmp( argv[parms], "-p" ) == 0 )
  861.             {
  862.                 optionsSpecified = true;
  863.                 gOptions |= kShowProgress;
  864.             }
  865.             else if( strcmp( argv[parms], "-q" ) == 0 )
  866.             {
  867.                 optionsSpecified = true;
  868.                 gOptions |= kDontQuote;
  869.             }
  870.             else if( strcmp( argv[parms], "-rev" ) == 0 )
  871.             {
  872.                 optionsSpecified = true;
  873.                 gOptions |= kOutputRevision;
  874.             }
  875.             else if( strcmp( argv[parms], "-textonly" ) == 0 )
  876.             {
  877.                 optionsSpecified = true;
  878.                 gOptions |= kTextOnly;
  879.             }
  880.             else if( strcmp( argv[parms], "-ignore" ) == 0 )
  881.             {
  882.                 optionsSpecified = true;
  883.                 ++parms;
  884.                 if(parms >= argc)
  885.                 {
  886.                     fprintf(stderr,"### %s - Encountered -ignore without a parameter", argv[0]);
  887.                 }
  888.                 else
  889.                 {
  890.                     processedParameters[parms] = true;
  891.                     gIgnoreList[gNumberOfIgnoreItems++] = argv[parms];
  892.                 }
  893.             }
  894.             else
  895.             {
  896.                 fprintf(stderr,"### %s - \"%s\" is not an option.\n", argv[0], argv[parms]);
  897.                 status = 1;
  898.                 
  899.                 break;
  900.             }
  901.         }
  902.     }
  903.     
  904.     //
  905.     // If neither -f nor -s, then -f
  906.     //
  907.     if((OptionSpecified(kFullPathnames) == false) && (OptionSpecified(kShortOutput) == false))
  908.         gOptions |= kFullPathnames;
  909.     
  910.     if(OptionSpecified(kShowProgress))
  911.     {
  912.         fprintf(stderr, "%s - Determine file's projector status.\n", argv[0]);
  913.         fprintf(stderr, "\t©1994, 1997 Apple Computer, Inc. by Greg Anderson\n");
  914.     }
  915.  
  916.     //
  917.     // If all of the "-" parameters were processed okay, then run through
  918.     // the parameters again looking for directory names
  919.     //
  920.     if(status == 0)
  921.     {
  922.         if((OptionSpecified(kShortOutput) == false))
  923.         {
  924.             putchar('\n');
  925.             fflush(stdout);
  926.         }
  927.  
  928.         for( parms = 1; parms < argc; parms++ )
  929.         {            
  930.             //
  931.             // If we didn't process this parameter the first time through the
  932.             // loop, then we assume it is the name of a directory search
  933.             // or a file to test
  934.             //
  935.             if(processedParameters[parms] == false)
  936.             {
  937.                 short length = strlen(argv[parms]);
  938.                 FSSpec tempFSSpec;
  939.                 
  940.                 strcpy((char *) gFilename+1, argv[parms]);
  941.                 gFilename[0] = length;
  942.                 
  943.                 //
  944.                 // Make an FSSpec so that we can get the vRefNum of the specified
  945.                 // directory.
  946.                 //
  947.                 err = FSMakeFSSpec(0, 0, gFilename, &tempFSSpec);
  948.                 if(err == noErr)
  949.                 {
  950.                     vRefNum = tempFSSpec.vRefNum;
  951.                     
  952.                     //
  953.                     // Use PBGetCatInfo to get the dirID of the specified folder
  954.                     //
  955.                     pb.dirInfo.ioCompletion            = nil;
  956.                     pb.dirInfo.ioNamePtr            = (StringPtr) &tempFSSpec.name;
  957.                     pb.dirInfo.ioResult                = noErr;
  958.                     pb.dirInfo.ioVRefNum            = vRefNum;
  959.                     pb.dirInfo.ioDrDirID            = tempFSSpec.parID;
  960.                     pb.dirInfo.ioFDirIndex            = 0;
  961.                     
  962.                     err = PBGetCatInfo(&pb,false);
  963.                 }
  964.                 
  965.                 //
  966.                 // Run Lurkers as soon as we get a directory or filename
  967.                 // (this is an easy and sleazy way to support multiple
  968.                 // folders/files on the command line)
  969.                 //
  970.                 if(err == noErr)
  971.                 {
  972.                     //
  973.                     // Is this a file or a folder?
  974.                     //
  975.                     if((pb.hFileInfo.ioFlAttrib & (1 << 4)) != 0)
  976.                     {
  977.                         dirID = pb.dirInfo.ioDrDirID;
  978.                         if(ItemIsInIgnoreList(tempFSSpec.name) == false)
  979.                         {
  980.                             Lurkers(vRefNum, dirID, dirID);
  981.                         }
  982.                     }
  983.                     else
  984.                     {
  985.                         Boolean unusedDidOutput = false;
  986.                         dirID = pb.hFileInfo.ioFlParID;
  987.                         
  988.                         //
  989.                         // If 'kTextOnly' is set, only process the file if its type
  990.                         // is 'TEXT'.  Otherwise, always process the file
  991.                         //
  992.                         if( (OptionSpecified(kTextOnly) == false) || (pb.hFileInfo.ioFlFndrInfo.fdType == 'TEXT'))
  993.                         {
  994.                             //
  995.                             // At this point, 'gFileName' contains the file exactly as
  996.                             // the user specified it. Copy the filename from the temporary
  997.                             // FSSpec, which contains only the name.
  998.                             //
  999.                             Size len = tempFSSpec.name[0] + 1;
  1000.                             BlockMove((Ptr)tempFSSpec.name, (Ptr)gFilename, len);
  1001.  
  1002.                             //
  1003.                             // Build the pathname to this folder if full paths were specified
  1004.                             //
  1005.                             gFolderpath[0] = 0;
  1006.                             if(OptionSpecified(kFullPathnames))
  1007.                             {
  1008.                                 BuildFolderPathname(vRefNum, dirID, gFolderpath, kMagicUnusedDirID, true);
  1009.                             }
  1010.                             // fprintf(stderr, "### About to process <%P> in folder <%P> dirID <%ld>\n", gFilename, gFolderpath, dirID);
  1011.     
  1012.                             ProcessFile(vRefNum, dirID, gFolderpath, gFilename, &unusedDidOutput);
  1013.                         }
  1014.                     }
  1015.                     
  1016.                     //
  1017.                     // Make a note of the fact that we ran lurkers
  1018.                     //
  1019.                     didLurkers = true;
  1020.                 }
  1021.                 else
  1022.                 {
  1023.                     //
  1024.                     // Set 'didLurkers' if we tried to run lurkers, too
  1025.                     //
  1026.                     fprintf(stderr, "### Error %d accessing %P\n", err, gFilename);
  1027.                     didLurkers = true;
  1028.                 }
  1029.             }
  1030.         }
  1031.     }
  1032.     
  1033.     //
  1034.     // if there were errors in the parameters, print usage
  1035.     //
  1036.     if((status == 1) || (didLurkers == false))
  1037.     {        
  1038.         fprintf(stderr, usage, argv[0]);
  1039.         
  1040.         //
  1041.         // Even more usage information for Andy...
  1042.         //
  1043.         fprintf(stderr, "\t©1994, 1997 Apple Computer, Inc. by Greg Anderson\n");
  1044.         fprintf(stderr, "\n");
  1045.         fprintf(stderr, "\tFile's projector status:\n");
  1046.         fprintf(stderr, "\t\t-insomeproject\t\t\t# File is checked in to some project (has ckid)\n");
  1047.         fprintf(stderr, "\t\t-notinanyproject\t\t# File is not checked in to any project (no ckid)\n");
  1048.         fprintf(stderr, "\t\t-checkedoutmodifiable\t# File is checked in to some project and is\n");
  1049.         fprintf(stderr, "\t\t\t\t\t\t\t\t#\tcurrently checked out for modification (but not MRO'ed)\n");
  1050.         fprintf(stderr, "\t\t-modifiableinproject\t# Synonym for -checkedoutmodifiable\n");
  1051.         fprintf(stderr, "\t\t-modifiable\t\t\t\t# Not in a project, in a project and checked out for modification,\n");
  1052.         fprintf(stderr, "\t\t\t\t\t\t\t\t#\tor in a project and checked out modified-read-only\n");
  1053.         fprintf(stderr, "\t\t-notmodifiable\t\t\t# In a project and not modifiable\n");
  1054.         fprintf(stderr, "\t\t-mro\t\t\t\t\t# In a project and modified-read-only\n");
  1055.         fprintf(stderr, "\n");
  1056.         fprintf(stderr, "\tOptions:\n");
  1057.         fprintf(stderr, "\t\t-r\t\t\t\t\t\t# Recursive search\n");
  1058.         fprintf(stderr, "\t\t-s\t\t\t\t\t\t# Short output\n");
  1059.         fprintf(stderr, "\t\t-q\t\t\t\t\t\t# Never quote filenames\n");
  1060.         fprintf(stderr, "\t\t-f\t\t\t\t\t\t# Output full pathnames\n");
  1061.         fprintf(stderr, "\t\t-rev\t\t\t\t\t# Output file's projector revision number (if any)\n");
  1062.         fprintf(stderr, "\t\t-textonly\t\t\t\t# Ignore files whose type is not TEXT\n");
  1063.         fprintf(stderr, "\t\t-ignore n\t\t\t\t# Ignore files and folders whose name is \"n\"\n");
  1064.         fprintf(stderr, "\t\t-p\t\t\t\t\t\t# Show progress info\n");
  1065.         fprintf(stderr, "\n");
  1066.         fprintf(stderr, "\tSpecifying a file tests only that file.  Specifying a directory tests every\n");
  1067.         fprintf(stderr, "\tfile in that directory (and every file anywhere in that hierarchy if -r is used).\n");
  1068.     }
  1069.     
  1070.     return status;
  1071. } // main 
  1072.